package com.example.demo.log;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONUtil;
import com.aliyun.oss.HttpMethod;
import com.example.demo.common.util.IPUtils;
import com.example.demo.model.form.LogForm;
import com.example.demo.security.util.SecurityUtils;
import com.example.demo.service.SysLogService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;

import java.util.Collection;
import java.util.Map;

/**
 * 操作日志切面
 * 用于记录系统操作日志，支持异步记录以提高性能
 *
 * @author demo
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {

    private final SysLogService sysLogService;
    private final CacheManager cacheManager;
    private final TaskExecutor taskExecutor;

    // ==================== 切点定义 ====================

    /**
     * 日志切点：拦截所有标注了@Log注解的方法
     */
    @Pointcut("@annotation(com.example.demo.log.Log)")
    public void logPointcut() {
    }

    // ==================== 核心方法 ====================

    /**
     * 环绕通知：记录操作日志
     *
     * @param joinPoint     切点
     * @param logAnnotation 日志注解
     * @return 方法执行结果
     * @throws Throwable 异常
     */
    @Around("logPointcut() && @annotation(logAnnotation)")
    public Object doAround(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable {
        // 在方法执行前获取上下文信息，避免在方法执行过程中清除上下文导致获取不到
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            log.warn("无法获取请求上下文，跳过日志记录。方法：{}", joinPoint.getSignature().toLongString());
            return joinPoint.proceed();
        }

        HttpServletRequest request = attributes.getRequest();
        String userId = SecurityUtils.getUserId();
        String methodSignature = joinPoint.getSignature().toLongString();
        String requestURI = request.getRequestURI();
        String requestMethod = request.getMethod();
        String userAgentString = request.getHeader("User-Agent");
        String ipAddr = IPUtils.getIpAddr(request);

        // 开始计时
        TimeInterval timer = DateUtil.timer();
        Object result = null;
        Exception exception = null;

        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            exception = e;
            throw e;
        } finally {
            // 计算执行时长
            long executionTime = timer.interval();

            // 异步保存日志，避免影响主流程性能
            Object finalResult = result;
            Exception finalException = exception;
            taskExecutor.execute(() -> {
                try {
                    saveLog(joinPoint, logAnnotation, methodSignature, requestURI, requestMethod,
                            userAgentString, ipAddr, finalResult, finalException, executionTime, userId);
                } catch (Exception e) {
                    log.error("异步保存日志失败，方法：{}", methodSignature, e);
                }
            });
        }

        return result;
    }

    // ==================== 日志保存相关方法 ====================

    /**
     * 保存日志
     *
     * @param joinPoint        切点
     * @param logAnnotation    日志注解
     * @param request          HTTP请求
     * @param methodSignature  方法签名
     * @param requestURI       请求URI
     * @param requestMethod    请求方法
     * @param userAgentString  User-Agent字符串
     * @param ipAddr           IP地址
     * @param result           方法执行结果
     * @param exception        异常信息
     * @param executionTime    执行时长（毫秒）
     * @param userId           用户ID
     */
    private void saveLog(JoinPoint joinPoint, Log logAnnotation,
                         String methodSignature, String requestURI, String requestMethod,
                         String userAgentString, String ipAddr, Object result, Exception exception,
                         long executionTime, String userId) {

        LogForm logForm = buildLogForm(joinPoint, logAnnotation, methodSignature, requestURI,
                requestMethod, userAgentString, ipAddr, result, exception, executionTime, userId);

        // 保存日志到数据库
        sysLogService.add(logForm);
    }

    /**
     * 构建日志表单对象
     *
     * @param joinPoint        切点
     * @param logAnnotation    日志注解
     * @param methodSignature  方法签名
     * @param requestURI       请求URI
     * @param requestMethod    请求方法
     * @param userAgentString  User-Agent字符串
     * @param ipAddr           IP地址
     * @param result           方法执行结果
     * @param exception        异常信息
     * @param executionTime    执行时长（毫秒）
     * @param userId           用户ID
     * @return 日志表单对象
     */
    private LogForm buildLogForm(JoinPoint joinPoint, Log logAnnotation,
                                 String methodSignature, String requestURI, String requestMethod,
                                 String userAgentString, String ipAddr, Object result, Exception exception,
                                 long executionTime, String userId) {

        LogForm logForm = new LogForm();
        logForm.setExecutionTime(executionTime);
        logForm.setRequestUri(requestURI);
        logForm.setMethod(methodSignature);
        logForm.setCreateBy(userId);

        // 处理异常情况
        if (exception != null && logAnnotation == null) {
            logForm.setModule(LogModuleEnum.EXCEPTION.getModuleName());
            logForm.setContent("系统发生异常");
            setRequestParameters(requestMethod, joinPoint, logForm);
            logForm.setResponseContent(JSONUtil.toJsonStr(exception.getStackTrace()));
        } else if (logAnnotation != null) {
            // 正常日志记录
            logForm.setModule(logAnnotation.module().getModuleName());
            logForm.setContent(logAnnotation.value());

            // 记录请求参数
            if (logAnnotation.params()) {
                setRequestParameters(requestMethod, joinPoint, logForm);
            }

            // 记录响应结果
            if (logAnnotation.result() && result != null) {
                logForm.setResponseContent(JSONUtil.toJsonStr(result));
            }
        }

        // 设置IP和地理位置信息
        setIpAndLocation(logForm, ipAddr);

        // 设置浏览器和系统信息
        setBrowserAndOsInfo(logForm, userAgentString);

        logForm.setCreateTime(DateUtil.date());
        return logForm;
    }

    /**
     * 设置IP地址和地理位置信息
     *
     * @param logForm 日志表单
     * @param ipAddr  IP地址
     */
    private void setIpAndLocation(LogForm logForm, String ipAddr) {
        if (StrUtil.isBlank(ipAddr)) {
            return;
        }

        logForm.setIp(ipAddr);
        String region = IPUtils.getRegion(ipAddr);
        if (StrUtil.isNotBlank(region)) {
            // 格式：中国|0|四川省|成都市|电信
            String[] regionArray = region.split("\\|");
            if (regionArray.length > 2) {
                logForm.setProvince(regionArray[2]);
                if (regionArray.length > 3) {
                    logForm.setCity(regionArray[3]);
                }
            }
        }
    }

    /**
     * 设置浏览器和操作系统信息
     *
     * @param logForm         日志表单
     * @param userAgentString User-Agent字符串
     */
    private void setBrowserAndOsInfo(LogForm logForm, String userAgentString) {
        if (StrUtil.isBlank(userAgentString)) {
            return;
        }

        UserAgent userAgent = resolveUserAgent(userAgentString);
        if (userAgent != null) {
            logForm.setOs(userAgent.getOs().getName());
            logForm.setBrowser(userAgent.getBrowser().getName());
            logForm.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
        }
    }

    // ==================== 请求参数处理相关方法 ====================

    /**
     * 设置请求参数到日志对象中
     *
     * @param requestMethod   HTTP方法
     * @param joinPoint 切点
     * @param logForm   日志表单
     */
    private void setRequestParameters(String requestMethod, JoinPoint joinPoint, LogForm logForm) {

        logForm.setRequestMethod(requestMethod);

        // GET、PUT、POST请求从方法参数获取
        if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod)
                || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod)
                || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) {
            String params = convertArgumentsToString(joinPoint.getArgs());
            logForm.setRequestParams(StrUtil.sub(params, 0, 65535));
        } else {
            // 其他请求方式从路径变量获取
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                Map<?, ?> paramsMap = (Map<?, ?>) attributes.getRequest()
                        .getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
                if (paramsMap != null) {
                    logForm.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535));
                } else {
                    logForm.setRequestParams("");
                }
            } else {
                logForm.setRequestParams("");
            }
        }
    }

    /**
     * 将参数数组转换为字符串
     *
     * @param paramsArray 参数数组
     * @return 参数字符串
     */
    private String convertArgumentsToString(Object[] paramsArray) {
        if (paramsArray == null || paramsArray.length == 0) {
            return "";
        }

        StringBuilder params = new StringBuilder();
        for (Object param : paramsArray) {
            if (!shouldFilterObject(param)) {
                params.append(JSONUtil.toJsonStr(param)).append(" ");
            }
        }
        return params.toString().trim();
    }

    /**
     * 判断是否需要过滤的对象
     * 过滤掉HttpServletRequest、HttpServletResponse、MultipartFile等对象
     *
     * @param obj 对象信息
     * @return 如果是需要过滤的对象，则返回true；否则返回false
     */
    private boolean shouldFilterObject(Object obj) {
        if (obj == null) {
            return true;
        }

        // 直接过滤的对象类型
        if (obj instanceof HttpServletRequest || obj instanceof HttpServletResponse || obj instanceof MultipartFile) {
            return true;
        }

        Class<?> clazz = obj.getClass();

        // 数组类型：检查数组元素类型
        if (clazz.isArray()) {
            return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
        }

        // 集合类型：检查集合元素
        if (Collection.class.isAssignableFrom(clazz)) {
            Collection<?> collection = (Collection<?>) obj;
            return collection.stream().anyMatch(item -> item instanceof MultipartFile);
        }

        // Map类型：检查Map的值
        if (Map.class.isAssignableFrom(clazz)) {
            Map<?, ?> map = (Map<?, ?>) obj;
            return map.values().stream().anyMatch(value -> value instanceof MultipartFile);
        }

        return false;
    }

    // ==================== UserAgent解析相关方法 ====================

    /**
     * 解析UserAgent
     * 使用缓存提高性能
     *
     * @param userAgentString User-Agent字符串
     * @return UserAgent对象
     */
    private UserAgent resolveUserAgent(String userAgentString) {
        if (StrUtil.isBlank(userAgentString)) {
            return null;
        }

        // 使用MD5作为缓存key，避免key过长
        String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString);
        Cache cache = cacheManager.getCache("userAgent");

        if (cache == null) {
            log.warn("userAgent缓存未配置，直接解析UserAgent");
            return UserAgentUtil.parse(userAgentString);
        }

        // 从缓存获取
        UserAgent userAgent = cache.get(userAgentStringMD5, UserAgent.class);
        if (userAgent != null) {
            return userAgent;
        }

        // 缓存未命中，解析并缓存
        userAgent = UserAgentUtil.parse(userAgentString);
        cache.put(userAgentStringMD5, userAgent);
        return userAgent;
    }
}
